# --- Data Exploration and Viz --- #
import pandas as pd
import plotly.express as px
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.preprocessing import StandardScaler
from resources.edautils import neg_pos_zero
from resources.customviz import expl_var
# --- Data Preprocessing --- #
import numpy as np
# --- Pipelines --- #
from resources.prep import \
build_prep, \
build_prep_3
from sklearn.cluster import KMeans, DBSCAN
from sklearn.neighbors import NearestNeighbors
# --- Model Loading --- #
import pickleData Masters Case: Agrupamentos Naturais
Felipe Viacava – São Paulo, ago/2023
O presente documento consiste no desenvolvimento de modelos de agrupamento como parte da solução do Case “Data Masters - Cientista de Dados” do Santander Brasil.
O objetivo é identificar e avaliar agrupamentos naturais (clusters), e atribuí-los a um rank baseado no lucro esperado por cliente.
Na etapa de classificação, buscava-se maximizar o lucro total que um modelo preditivo poderia gerar ao banco numa campanha de retenção. Agrupamentos naturais, por outro lado, são criados de forma não supervisionada, de modo que não podemos utilizar falsos positivos e verdadeiros positivos encontrados nos modelos para avaliá-los, uma vez que não existe uma variável “TARGET” para determinar estas métricas.
Premissas adotadas nesta etapa:
Adota-se o lucro esperado por cliente calculado na etapa anterior;
Novo pipeline de preprocessamento pois, em geral, modelos de agrupamento são sensíveis à distribuições quando tratamos de variáveis numéricas;
Os clusters são encontrados usando todos os dados, mas o lucro per capita só é avaliado sobre a base de testes.
Bibliotecas
In [1]:
Lucro estimado
Uma das vantagens de trabalhar com classes para criar modelos robustos é a portabilidade. Não apenas para fins de deployment, mas também para agilidade em seu uso para diferentes aplicações. Aqui, lemos o conjunto de testes e carregamos o modelo campeão previamente treinado para recriar a coluna de lucro esperado por cliente.
In [2]:
Mostrar/esconder código
with open("models/hgb.pkl", "rb") as f:
hgb = pickle.load(f)
test = pd \
.read_csv('data/test.csv') \
.assign(
predicted = (
lambda ldf:
hgb.predict(ldf.drop("TARGET", axis=1))
),
profit = (
lambda ldf:
((ldf["TARGET"] * 100) - 10) * ldf["predicted"]
),
origin = "test"
)
train = pd \
.read_csv('data/train.csv') \
.assign(
predicted = np.nan,
profit = np.nan,
origin = "train"
)
df = pd.concat([train, test]).reset_index(drop=True)
reference = df[["ID", "TARGET", "predicted", "profit", "origin"]]
pd.concat([reference.head(3), reference.tail(3)]).head(6)| ID | TARGET | predicted | profit | origin | |
|---|---|---|---|---|---|
| 0 | 113911 | 0 | NaN | NaN | train |
| 1 | 120462 | 0 | NaN | NaN | train |
| 2 | 87126 | 0 | NaN | NaN | train |
| 76017 | 138601 | 1 | 0.0 | 0.0 | test |
| 76018 | 78655 | 0 | 1.0 | -10.0 | test |
| 76019 | 130139 | 0 | 1.0 | -10.0 | test |
Processamento
Os passos de pré-processamento dos dados utilizados no modelo campeão serão reutilizados aqui, com exceção dos encoders ordinais. Na classificação, foram usados enconders ordinais para evitar o aumento de dimensionalidade, reduzindo o número de variáveis aleatórias necessárias por split nas árvores, uma vez que lidam bem com relações não lineares entre variáveis independentes e a variável target. No caso da análise de clusters, foi escolhido o One Hot Encoding para as features categóricas, além de outras manipulações para as variáveis numéricas.
In [3]:
Mostrar/esconder código
pdf = df.drop(["TARGET","predicted","profit","origin"], axis=1)
prep = build_prep()[:-2].fit(pdf)
pdf = prep.transform(pdf)
prepPipeline(steps=[('DropConstantColumns', DropConstantColumns(also=['ID'])),
('DropDuplicateColumns', DropDuplicateColumns()),
('NoneZeroCountSaldo', AddNonZeroCount(prefix='saldo')),
('SumSaldo', CustomSum(prefix='saldo')),
('NoneZeroCountImp', AddNonZeroCount(prefix='imp')),
('SumImp', CustomSum(prefix='imp')),
('ImputeNanDelta',
CustomImputer(prefix='delta', to...99999)),
('NoneCountDelta', AddNoneCount(prefix='delta')),
('NonZeroCountDelta', AddNonZeroCount(prefix='delta')),
('SumDelta', CustomSum(prefix='delta')),
('NonZeroContInd', AddNonZeroCount(prefix='ind')),
('NonZeroCountNum', AddNonZeroCount(prefix='num')),
('SumNum', CustomSum(prefix='num')),
('ImputeNanVar3',
CustomImputer(prefix='var3', to_replace=-999999))])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Pipeline(steps=[('DropConstantColumns', DropConstantColumns(also=['ID'])),
('DropDuplicateColumns', DropDuplicateColumns()),
('NoneZeroCountSaldo', AddNonZeroCount(prefix='saldo')),
('SumSaldo', CustomSum(prefix='saldo')),
('NoneZeroCountImp', AddNonZeroCount(prefix='imp')),
('SumImp', CustomSum(prefix='imp')),
('ImputeNanDelta',
CustomImputer(prefix='delta', to...99999)),
('NoneCountDelta', AddNoneCount(prefix='delta')),
('NonZeroCountDelta', AddNonZeroCount(prefix='delta')),
('SumDelta', CustomSum(prefix='delta')),
('NonZeroContInd', AddNonZeroCount(prefix='ind')),
('NonZeroCountNum', AddNonZeroCount(prefix='num')),
('SumNum', CustomSum(prefix='num')),
('ImputeNanVar3',
CustomImputer(prefix='var3', to_replace=-999999))])DropConstantColumns(also=['ID'])
DropDuplicateColumns()
AddNonZeroCount(prefix='saldo')
CustomSum(prefix='saldo')
AddNonZeroCount(prefix='imp')
CustomSum(prefix='imp')
CustomImputer(prefix='delta', to_replace=9999999999)
AddNoneCount(prefix='delta')
AddNonZeroCount(prefix='delta')
CustomSum(prefix='delta')
AddNonZeroCount(prefix='ind')
AddNonZeroCount(prefix='num')
CustomSum(prefix='num')
CustomImputer(prefix='var3', to_replace=-999999)
In [4]:
Mostrar/esconder código
npz = neg_pos_zero(pdf, list(pdf.columns))
npz \
.assign(
zero_zone = lambda ldf: ldf["Zero values (%)"].apply(lambda lr: lr//10 * 10)
) \
[["zero_zone", "Column"]] \
.groupby("zero_zone") \
.count() \
.rename(mapper={"Column": "Number of Columns"}, axis=1)| Number of Columns | |
|---|---|
| zero_zone | |
| 0.0 | 12 |
| 10.0 | 5 |
| 20.0 | 10 |
| 30.0 | 7 |
| 50.0 | 1 |
| 60.0 | 4 |
| 70.0 | 4 |
| 80.0 | 26 |
| 90.0 | 247 |
In [5]:
remaining = [
col
for col in pdf.columns
if ((pdf[col]==0).sum()/pdf.shape[0] < .4)
]
remaining['var3',
'var15',
'ind_var5_0',
'ind_var5',
'ind_var30_0',
'ind_var30',
'ind_var39_0',
'ind_var41_0',
'num_var4',
'num_var5_0',
'num_var5',
'num_var30_0',
'num_var30',
'num_var35',
'num_var39_0',
'num_var41_0',
'num_var42_0',
'num_var42',
'saldo_var5',
'saldo_var30',
'saldo_var42',
'var36',
'num_meses_var5_ult3',
'num_meses_var39_vig_ult3',
'saldo_medio_var5_hace2',
'saldo_medio_var5_hace3',
'saldo_medio_var5_ult1',
'saldo_medio_var5_ult3',
'var38',
'non_zero_count_saldo',
'sum_of_saldo',
'non_zero_count_ind',
'non_zero_count_num',
'sum_of_num']
In [6]:
Mostrar/esconder código
ss = StandardScaler()
pdft = pd.concat(
[
pd.DataFrame(
ss.fit_transform(pdf),
columns=pdf.columns
),
reference
],
axis=1
)
long_df = pdft \
.melt(
id_vars="TARGET",
value_vars=remaining,
var_name="variable",
value_name="value"
)
plt.figure(figsize=(10, 30))
sns.violinplot(
long_df,
x="value",
y="variable",
)
plt.show()
Processamento: PCA
In [7]:
Mostrar/esconder código
cdf = df.drop(["TARGET","predicted","profit","origin"],axis=1)
prep = build_prep_3().fit(cdf)
tdf = pd.concat(
[
pd.DataFrame(prep.transform(cdf)),
reference
],
axis=1
)
prepPipeline(steps=[('prep',
Pipeline(steps=[('DropConstantColumns',
DropConstantColumns(also=['ID'])),
('DropDuplicateColumns',
DropDuplicateColumns()),
('NoneZeroCountSaldo',
AddNonZeroCount(prefix='saldo')),
('SumSaldo', CustomSum(prefix='saldo')),
('NoneZeroCountImp',
AddNonZeroCount(prefix='imp')),
('SumImp', CustomSum(prefix='imp')),
('ImputeNanDelta',
CustomI...
'saldo_medio_var5_ult1',
'saldo_medio_var5_ult3', 'num_var42_0',
'sum_of_saldo', 'var38', 'sum_of_num',
'non_zero_count_num',
'non_zero_count_ind'])),
('cat',
ColumnTransformer(remainder='passthrough',
transformers=[('ohe',
Pipeline(steps=[('ohe',
OneHotEncoder(min_frequency=100,
sparse_output=False))]),
['var36'])])),
('ss', StandardScaler()), ('knn', KNNImputer()),
('pca', PCA())])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Pipeline(steps=[('prep',
Pipeline(steps=[('DropConstantColumns',
DropConstantColumns(also=['ID'])),
('DropDuplicateColumns',
DropDuplicateColumns()),
('NoneZeroCountSaldo',
AddNonZeroCount(prefix='saldo')),
('SumSaldo', CustomSum(prefix='saldo')),
('NoneZeroCountImp',
AddNonZeroCount(prefix='imp')),
('SumImp', CustomSum(prefix='imp')),
('ImputeNanDelta',
CustomI...
'saldo_medio_var5_ult1',
'saldo_medio_var5_ult3', 'num_var42_0',
'sum_of_saldo', 'var38', 'sum_of_num',
'non_zero_count_num',
'non_zero_count_ind'])),
('cat',
ColumnTransformer(remainder='passthrough',
transformers=[('ohe',
Pipeline(steps=[('ohe',
OneHotEncoder(min_frequency=100,
sparse_output=False))]),
['var36'])])),
('ss', StandardScaler()), ('knn', KNNImputer()),
('pca', PCA())])Pipeline(steps=[('DropConstantColumns', DropConstantColumns(also=['ID'])),
('DropDuplicateColumns', DropDuplicateColumns()),
('NoneZeroCountSaldo', AddNonZeroCount(prefix='saldo')),
('SumSaldo', CustomSum(prefix='saldo')),
('NoneZeroCountImp', AddNonZeroCount(prefix='imp')),
('SumImp', CustomSum(prefix='imp')),
('ImputeNanDelta',
CustomImputer(prefix='delta', to...99999)),
('NoneCountDelta', AddNoneCount(prefix='delta')),
('NonZeroCountDelta', AddNonZeroCount(prefix='delta')),
('SumDelta', CustomSum(prefix='delta')),
('NonZeroContInd', AddNonZeroCount(prefix='ind')),
('NonZeroCountNum', AddNonZeroCount(prefix='num')),
('SumNum', CustomSum(prefix='num')),
('ImputeNanVar3',
CustomImputer(prefix='var3', to_replace=-999999))])DropConstantColumns(also=['ID'])
DropDuplicateColumns()
AddNonZeroCount(prefix='saldo')
CustomSum(prefix='saldo')
AddNonZeroCount(prefix='imp')
CustomSum(prefix='imp')
CustomImputer(prefix='delta', to_replace=9999999999)
AddNoneCount(prefix='delta')
AddNonZeroCount(prefix='delta')
CustomSum(prefix='delta')
AddNonZeroCount(prefix='ind')
AddNonZeroCount(prefix='num')
CustomSum(prefix='num')
CustomImputer(prefix='var3', to_replace=-999999)
DropConstantColumns(search=0, thresh=0.4)
CustomLog(columns=['var3', 'saldo_var30', 'saldo_var42',
'saldo_medio_var5_hace2', 'saldo_medio_var5_hace3',
'saldo_medio_var5_ult1', 'saldo_medio_var5_ult3',
'num_var42_0', 'sum_of_saldo', 'var38', 'sum_of_num',
'non_zero_count_num', 'non_zero_count_ind'])ColumnTransformer(remainder='passthrough',
transformers=[('ohe',
Pipeline(steps=[('ohe',
OneHotEncoder(min_frequency=100,
sparse_output=False))]),
['var36'])])['var36']
OneHotEncoder(min_frequency=100, sparse_output=False)
['var3', 'var15', 'ind_var5_0', 'ind_var5', 'ind_var30_0', 'ind_var30', 'ind_var39_0', 'ind_var41_0', 'num_var4', 'num_var5_0', 'num_var5', 'num_var30_0', 'num_var30', 'num_var35', 'num_var39_0', 'num_var41_0', 'num_var42_0', 'num_var42', 'saldo_var5', 'saldo_var30', 'saldo_var42', 'num_meses_var5_ult3', 'num_meses_var39_vig_ult3', 'saldo_medio_var5_hace2', 'saldo_medio_var5_hace3', 'saldo_medio_var5_ult1', 'saldo_medio_var5_ult3', 'var38', 'non_zero_count_saldo', 'sum_of_saldo', 'non_zero_count_ind', 'non_zero_count_num', 'sum_of_num']
passthrough
StandardScaler()
KNNImputer()
PCA()
In [8]:
Mostrar/esconder código
expl_var(prep[-1].explained_variance_ratio_)
80% of variance is explained by 9 components
In [9]:
Mostrar/esconder código
long_df = tdf \
.melt(
id_vars="TARGET",
value_vars=[0, 1, 2],
var_name="variable",
value_name="value"
)
sns.violinplot(
long_df,
x="variable",
y="value",
)
plt.show()
In [10]:
Mostrar/esconder código
px.scatter_3d(
tdf.assign(TARGET=tdf["TARGET"].astype("category")),
x=0,
y=1,
z=2,
color="TARGET",
opacity=.1
)Agrupamentos com KMeans
In [11]:
Mostrar/esconder código
ssd = []
for num_clusters in range(1, 31):
kmeans = KMeans(n_clusters=num_clusters, random_state=42, n_init='auto')
kmeans.fit(tdf[[i for i in range(0,10)]])
ssd.append(kmeans.inertia_)
plt.figure(figsize=(10,6))
plt.plot(range(1, 31), ssd, marker='o', linestyle='--')
plt.xlabel('Number of Clusters')
plt.ylabel('Sum of Squared Distances')
plt.title('Elbow Method For Optimal Number of Clusters')
plt.show()
In [12]:
Mostrar/esconder código
kmeans = KMeans(n_clusters=9, random_state=42, n_init='auto')
clusters = kmeans.fit_predict(tdf[[i for i in range(0,10)]])
tdf["kmeans"] = clusters
px.scatter_3d(
tdf.assign(kmeans=tdf["kmeans"].astype("category")),
x=0,
y=1,
z=2,
color="kmeans",
opacity=.1
)In [13]:
Mostrar/esconder código
gdf = tdf[["kmeans", "profit", "TARGET"]] \
.groupby(["kmeans"]) \
.agg(["mean", "sum", "count"])
gdf.columns = [
"Average Profit",
"Total Profit",
"Number of Customers (Test)",
"drop0",
"drop1",
"Number of Customers (Total)"
]
gdf \
.drop(["drop0", "drop1"], axis=1) \
.sort_values("Average Profit", ascending=False)| Average Profit | Total Profit | Number of Customers (Test) | Number of Customers (Total) | |
|---|---|---|---|---|
| kmeans | ||||
| 5 | 4.137623 | 4630.0 | 1119 | 4454 |
| 4 | 2.860114 | 11000.0 | 3846 | 15372 |
| 7 | 1.495935 | 920.0 | 615 | 2423 |
| 8 | 0.550459 | 60.0 | 109 | 406 |
| 6 | 0.425601 | 1310.0 | 3078 | 12306 |
| 1 | 0.104572 | 780.0 | 7459 | 29807 |
| 0 | 0.000000 | 0.0 | 1318 | 5465 |
| 2 | 0.000000 | 0.0 | 419 | 1633 |
| 3 | -0.038388 | -40.0 | 1042 | 4154 |